import AppSettings from "@runtime/base/AppSettings";
import MessageHandler from "../Handler/MessageHandler";
import RequestHandler from "../Handler/RequestHandler";
import OpcodeComponent from "../Opcode/OpcodeComponent";
import ProtobufComponent from "../Protobuf/ProtobufComponent";
import TimerComponent from "../Timer/TimerComponent";

export default class WebSocketComponent implements Chaos.IDisposable {
    private static instance: WebSocketComponent = new WebSocketComponent();
    public static get Default(): WebSocketComponent {
        return this.instance;
    }

    constructor() {
        window.addEventListener('unload', this.Dispose.bind(this));

        this.Reconnect();
    }

    Dispose() {
        window.removeEventListener('unload', this.Dispose);

        this.rpcId = -1;
        this.requestCallbacks.clear();
        this.handlers.clear();

        this.url = null;
        this.socket?.close();
        this.socket = null;
    }

    private rpcId: number = -1;
    private requestCallbacks: Map<number, Function> = new Map();
    private handlers: Map<number, Array<Chaos.IMessageHandler>> = new Map();

    private url: string | null = null;
    private socket: WebSocket | null = null;

    public SingleMachineCommunicationServerKey: string = 'LocalAddress';

    private OnOpen(event: Event): void {
        window.Logger.Information(`会话已连接`);
    }

    private OnError(event: Event): void {
        window.Logger.Error('会话连接异常');
    }

    private OnClose(event: CloseEvent): void {
        window.Logger.Information(`会话已断开(code:${event.code},reason:${event.reason},wasClean:${event.wasClean})`);
    }

    private OnMessage(event: MessageEvent): void {
        let data = new Uint8Array(event.data);

        let opcodeView = new DataView(data.buffer, 8, 2);
        let opcode = opcodeView.getUint16(0, true);
        const message = ProtobufComponent.Default.Deserialize(opcode, data.slice(8 + 2));

        this.OnRead(opcode, message);
    }

    private OnRead<TMessage extends Chaos.IMessage | Chaos.IResponse>(opcode: number, message: TMessage): void {
        OpcodeComponent.Default.LogMsg('Dispatch', opcode, message);

        for (const handler of this.handlers.get(opcode) ?? []) {
            handler.HandleAsync(message);
        }

        if (Object.IsOfType<Chaos.IResponse>(message, 'RpcId')) {
            this.requestCallbacks.get(message.RpcId)?.(message);
            this.requestCallbacks.delete(message.RpcId);
        }
    }

    private Reconnect(): void {
        const address = AppSettings.Default.LocalAddress;
        if (address == '') {
            return;
        }

        this.url = `ws://${address}/ws`;

        this.socket = new WebSocket(this.url);

        this.socket.binaryType = 'arraybuffer';

        this.socket.addEventListener('open', event => this.OnOpen(event));
        this.socket.addEventListener('error', event => this.OnError(event));
        this.socket.addEventListener('close', event => this.OnClose(event));
        this.socket.addEventListener('message', event => this.OnMessage(event));
    }

    public async ConnectAsync(): Promise<boolean> {
        const address = AppSettings.Default.LocalAddress;
        if (address == '') {
            return false;
        }

        this.url = `ws://${address}/ws`;

        this.socket = new WebSocket(this.url);

        this.socket.binaryType = 'arraybuffer';

        this.socket.addEventListener('open', event => this.OnOpen(event));
        this.socket.addEventListener('error', event => this.OnError(event));
        this.socket.addEventListener('close', event => this.OnClose(event));
        this.socket.addEventListener('message', event => this.OnMessage(event));

        while (true) {
            if (this.socket.readyState == WebSocket.CLOSING || this.socket.readyState == WebSocket.CLOSED) {
                return false;
            }
            else if (this.socket.readyState == WebSocket.OPEN) {
                return true;
            }
            else {
                await TimerComponent.Default.WaitAsync(10);
            }
        }
    }

    public PostMessage<TMessage extends Chaos.IMessage>(message: TMessage & { Opcode: number }): void {
        if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
            window.Logger.Error(`会话已断开`);
            return;
        }

        const opcode = message.Opcode;

        let buffer = ProtobufComponent.Default.Serialize(message);

        let data = new ArrayBuffer(buffer.byteLength + 8 + 2);

        let opcodeView = new DataView(data, 8, 2);
        opcodeView.setUint16(0, opcode, true);

        let bufferView = new Uint8Array(data, 8 + 2, buffer.byteLength);
        bufferView.set(buffer);

        OpcodeComponent.Default.LogMsg('Send', opcode, message);

        this.socket.send(data);
    }

    public PostMessageAsync<TRequest extends Chaos.IRequest, TResponse extends Chaos.IResponse>(request: TRequest & { Opcode: number }): Promise<TResponse> {
        const opcode = request.Opcode;

        request.RpcId = ++this.rpcId;

        return new Promise<TResponse>((resolve: (value: TResponse) => void) => {
            if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
                resolve(Object.assign({ RpcId: request.RpcId, Error: -1, Message: '会话已断开' }));
            } else {
                this.requestCallbacks.set(request.RpcId!, resolve);

                let buffer = ProtobufComponent.Default.Serialize(request);

                let data = new ArrayBuffer(buffer.byteLength + 8 + 2);

                let opcodeView = new DataView(data, 8, 2);
                opcodeView.setUint16(0, opcode, true);

                let bufferView = new Uint8Array(data, 8 + 2, buffer.byteLength);
                bufferView.set(buffer);

                OpcodeComponent.Default.LogMsg('Send', opcode, request);

                this.socket.send(data);
            }
        });
    }

    public Register<TMessage extends Chaos.IMessage | Chaos.IActorMessage, TRequest extends Chaos.IRequest | Chaos.IActorRequest, TResponse extends (Chaos.IResponse | Chaos.IActorResponse) & { Opcode: number }>(opcode: number, handler: MessageHandler<TMessage> | RequestHandler<TRequest, TResponse>): void {
        if (!this.handlers.has(opcode)) {
            this.handlers.set(opcode, new Array<Chaos.IMessageHandler>());
        }

        this.handlers.get(opcode)?.add(handler);
    }

    public Reply(response: (Chaos.IResponse | Chaos.IActorResponse) & { Opcode: number }): void {
        if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
            window.Logger.Error(`会话已断开`);
            return;
        }
        const opcode = response.Opcode;

        let buffer = ProtobufComponent.Default.Serialize(response);

        let data = new ArrayBuffer(buffer.byteLength + 8 + 2);

        let opcodeView = new DataView(data, 8, 2);
        opcodeView.setUint16(0, opcode, true);

        let bufferView = new Uint8Array(data, 8 + 2, buffer.byteLength);
        bufferView.set(buffer);

        OpcodeComponent.Default.LogMsg('Reply', opcode, response);

        this.socket.send(data);
    }
}